<?php

class Test extends AppModel
{

	public $name = 'Test';

	public $displayField = 'name';

	protected $types = array(
	   'SIMPLE',
	   'CONSOLIDATED',
	);

	public $validate = array(
        'name' => array(
            'create_first' => array(
                'on' => 'create',
                'required' => true,
                'allowEmpty' => false,
                'message' => 'required',
            ),
            'update_first' => array(
                'on' => 'update',
                'required' => false,
                'allowEmpty' => false,
                'message' => 'required',
            ),
        ),
	);

	public $hasAndBelongsToMany = array(
        'Clients' => array('className' => 'Client'),
        'Reports' => array('className' => 'Report', 'joinTable' => 'tests_reports', 'conditions' => array('Reports.type' => 'SIMPLE')),
        'ConsReports' => array('className' => 'Report', 'joinTable' => 'tests_reports', 'conditions' => array('ConsReports.type' => 'CONSOLIDATED')),
	);

	public $belongsTo = array(
        'TestGroup' => array('className' => 'TestGroup'),
	);

	public $hasMany = array(
        'Testings' => array('className' => 'Testing'),
	);

	function allowDelete($fields = null)
	{
        return false;
	}

	function allowUpdate($fields = null)
	{
        return true;
	}

	/**
	 * Обработчик события перед выборкой, принимающий условия выборки по ссылке для их изменения.
	 *
	 * @param array $conds
	 */
	protected function modifyConditions(&$conds)
	{
	    if (Auth::o()->is('client_admin')) {
            $auth_data = Auth::o()->data();
            $client_id = $auth_data['client_id'];

	        $Client =& $this->getModel('Client'); /* @var $Client Client */
            $Client->rel();
            $client = $Client->read(null, $client_id);
            $conds[]["`{$this->name}`.`{$this->primaryKey}`"] = isset($client[$Client->name]['tools']) ? $client[$Client->name]['tools'] : array();
	    }
	}

	function generateList($conds = array())
	{
	    $this->rel();
        return parent::generateList($conds, "{$this->name}.{$this->primaryKey}", null, "{n}.{$this->name}.{$this->primaryKey}", "{n}.{$this->name}.{$this->displayField}");
	}

	/**
	 * Обработчик события после выборки, принимающий атрибуты каждой выбраной записи по ссылке для их изменения.
	 *
	 * @param &array $row
	 * @param bool $primary
	 */
	function modifyRow(&$row, $primary)
	{
	    if (!empty($row['tool_file_name']) && !empty($row['type'])) {
    	    if ($row['type'] == 'TEST') {

    			if (vendor('instrument/'.$row['tool_file_name']))
    			{
    				$t = new $row['tool_class_name'];
    				$row['Norms'] = $t->getNormasList();
    			}
    	    }
	    }
	}

	function getNorm($toolFileName, $toolClassName, $norm_id/*$norm_id, $test_id = null*/)
	{
		if (vendor('instrument/'.$toolFileName))
		{
			$t = new $toolClassName;
			return $t->getNorma($norm_id);
		}
		else
		{
			die("Not found: ".$row['tool_file_name']);
		}
	}

	function getForConsReports($conds = array(), $fields = null, $order = null)
	{
		$Report =& $this->getModel('Report'); /* @var $Report Report */
		$Report->rel('Tests');
		$reports = $Report->findAll(array("`{$Report->name}`.`type`" => 'CONSOLIDATED'), array("`{$Report->name}`.`{$Report->primaryKey}`"));

		$consolidated_tests_id = array();
		if (! empty($reports)) {
			foreach ($reports as $report) {
				if (! empty($report['Tests'])) {
					foreach ($report['Tests'] as $test) {
						$consolidated_tests_id[$test[$this->primaryKey]] = true;
					}
				}
			}
			if (count($consolidated_tests_id)) {
				$consolidated_tests_id = array_keys($consolidated_tests_id);
			}
		}

		$result = array();
		if (count($consolidated_tests_id)) {
			$this->rel('ConsReports');
			$conds[]["`{$this->name}`.`{$this->primaryKey}`"] = $consolidated_tests_id;
			$result = $this->findAll($conds, $fields, $order);

			foreach ($result as &$test) {
				$test['Test']['Reports'] = $test['ConsReports'];
				unset($test['ConsReports']);
			}
		}

		return $result;
	}

	function getParamsFor($name)
	{
	    $ToolParams = new ToolParams($name);
	    return $ToolParams->getParams();
	}

	/**
	 * Возвращает номер ответа по его строковому односимвольному представлению.
	 *
	 * @param string/str $answer
	 * @return int
	 */
	private static function getAnswerNumber($answer)
	{
        $answer = str($answer)->eregiReplace("[^0-9a-z]", "")->toLowerCase()->convertTo("ISO-8859-1")->value;
        if (strlen($answer) == 0) {
            $answer = "0";
        } else {
            $answer = $answer[0];
        }
        if (is_numeric($answer)) {
            $answer = intval($answer);
        } else {
            $answer = ord($answer) - ord("a") + 10;
        }

        return $answer;
	}

	/**
	 * Тоже самое, только изменяет параметр по ссылке.
	 *
	 * @param string $answer
	 */
	static function getAnswerNumberRef(&$answer)
	{
        $answer = self::getAnswerNumber($answer);
	}

	/**
	 * Возвращает строковое односимвольное представление ответа по его номеру.
	 *
	 * @param int $answer
	 * @return string
	 */
	static function getAnswerByNumber($answer)
	{
	    if (! is_int($answer) || ($answer < 0) || ($answer > 35)) {
	        throw new ArgException();
	    }
	    if ($answer < 10) {
            return strval($answer);
	    } else {
	        return chr($answer + ord("a") - 10);
	    }
	}

	/**
	 * Тоже самое, только изменяет параметр по ссылке.
	 *
	 * @param int $answer
	 */
	static function getAnswerByNumberRef(&$answer)
	{
        $answer = self::getAnswerByNumber($answer);
	}

	/**
	 * Тоже, что и intval() (с параметром $base по-умолчанию), только изменяет параметр по ссылке.
	 *
	 * @param mixed $answer
	 */
	static function intvalRef(&$value)
	{
        $value = intval($value);
	}

	/**
	 * Возвращает строковое представление ответов.
	 * Проводит нормализацию.
	 *
	 * @param string $name Идентификатор (название) инструмента.
	 * @param array/string $answers
	 * @return string
	 */
	function packAnswersToStringFor($name, $answers)
	{
	    $toolParams = $this->getParamsFor($name);
	    $this->normalizeAnswersFor($name, $answers);
	    if (is_null($answers)) {
	        return "";
	    }

	    $toolParamsObj = null;
	    if ($toolParams["mode"] == "BIN_COMBS") {
            $toolParamsObj = new ToolParams($name);
	    }
        foreach ($answers as $i => &$ans) {
            if ($toolParams["mode"] == "OPPOSITE") {
                array_walk($ans, array($this, 'getAnswerByNumberRef'));
                $ans = implode('', $ans);
            } elseif (($toolParams["mode"] == "BIN_COMBS") && (in_array($i, $toolParams["binaryCombinationsQuestions"]))) {
                if (count($ans) > 1) {
                    $ans = self::getAnswerByNumber(
                        $toolParamsObj->getAnswerNumByBinaryCombination($ans[0], $ans[1])
                    );
                } else {
                    $ans = $this->getAnswerByNumber($ans[0]);
                }
            }
        }

        $addZero = ((($toolParams["questionsNumber"] * $toolParams["checksNumber"] + 2) % 2) != 0)
            ? "0"
            : "";
        return implode('', $answers).$addZero;
	}

	/**
	 * Нормализует ответы на указанный инструмент тестирования.
	 * В зависимости от типа инструмента возвращает по ссылке двух- или трёхмерный
	 * массив ответов (односимвольное представление).
	 * Также возвращает массив с номерами отсуцтвующих ответов.
	 *
	 * @param string $name Идентификатор (название) инструмента.
	 * @param array/string $answers
	 * @return array
	 */
	function normalizeAnswersFor($name, &$answers)
	{
	    $toolParams = $this->getParamsFor($name);
	    if (empty($toolParams["questionsNumber"])) {
	        return null;
	    }

	    if (is_string($answers)) {
	        $answers = $answers = str($answers)->eregiReplace("[^0-9a-z]", "")->toLowerCase()->convertTo("ISO-8859-1")->value;
            if (! empty($toolParams["checksNumber"]) && ($toolParams["checksNumber"] > 1)) {
                $answers = str_split($answers, $toolParams["checksNumber"]);
                foreach ($answers as &$ans) {
                    $ans = str_split($ans);
                    array_walk($ans, array($this, 'getAnswerNumberRef'));
                    $ans = array_combine(range(1, count($ans)), $ans);
                }
            } else {
                $answers = str_split($answers);
                array_walk($answers, array($this, 'getAnswerNumberRef'));
                $answers = array_combine(range(1, count($answers)), $answers);
                if ($toolParams["mode"] === "BIN_COMBS") {
                    $toolParamsObj = new ToolParams($name);
                    foreach ($answers as $i => &$answer) {
                        if (in_array($i, $toolParams["binaryCombinationsQuestions"])) {
                            try {
                                $answer = $toolParamsObj->getAnswerBinaryCombination($answer);
                            } catch (ArgException $e) {
                                $answer = array(0);
                            }
                        }
                    }
                }
            }
	    } elseif (is_array($answers)) {
	        array_walk_recursive($answers, array($this, 'intvalRef'));
	    }

	    $newAnswers = array();
        $notFilling = array();
        for ($i = 1; $i <= $toolParams["questionsNumber"]; $i++) {
            if (isset($toolParams["checksNumber"]) && ($toolParams["checksNumber"] > 1)) {
                if (isset($answers[$i]) && is_array($answers[$i])) {
                    $selected_values = array();
                    for ($k = 1; $k <= $toolParams["checksNumber"]; $k++) {
                        if (isset($answers[$i][$k])) {
                            $answers[$i][$k] = intval($answers[$i][$k]);
                            if (($answers[$i][$k] < 1) || ($answers[$i][$k] > $toolParams["maxAnswerNumber"]) || in_array($answers[$i][$k], $selected_values)) {
                                $answers[$i][$k] = 0;
                                $notFilling[$i][$k] = true;
                            } else {
                                $selected_values[] = $answers[$i][$k];
                            }
                        } else {
                            $answers[$i][$k] = 0;
                            $notFilling[$i][$k] = true;
                        }
                    }
                } else {
                    $answers[$i] = array_fill(1, $toolParams["checksNumber"], 0);
                    $notFilling[$i] = array_fill(1, $toolParams["checksNumber"], true);
                }
            } else {
                if (isset($answers[$i])) {
                    if (($toolParams["mode"] === "BIN_COMBS") && in_array($i, $toolParams["binaryCombinationsQuestions"])) {
                        if (isset($answers[$i]) && is_array($answers[$i])) {
                            $tmp = array();
                            foreach ($answers[$i] as $tmp_answer) {
                                if (($tmp_answer >= 1) && ($tmp_answer <= $toolParams["answersNumber"])) {
                                    $tmp[] = $tmp_answer;
                                }
                            }
                            $answers[$i] = $tmp;
                            if (isset($answers[$i][0])) {
                                $primaryAnswer = $answers[$i][0];
                                $secondaryAnswer = null;
                                if (isset($answers[$i][1])) {
                                    $secondaryAnswer = $answers[$i][1];
                                }
                                $answers[$i] = array($primaryAnswer);
                                if (isset($secondaryAnswer)) {
                                        if (in_array($secondaryAnswer, $toolParams["binaryCombinations"][$primaryAnswer])) {
                                        $answers[$i][] = $secondaryAnswer;
                                        } else {
                                            $answers[$i] = array(0);
                                            $notFilling[$i] = true;
                                        }
                                    }
                                } else {
                                    $answers[$i] = array(0);
                                    $notFilling[$i] = true;
                                }
                            } else {
                                $answers[$i] = array(0);
                                $notFilling[$i] = true;
                            }
                        } else {
                        if (($answers[$i] < 1) || ($answers[$i] > $toolParams["maxAnswerNumber"])) {
                            $answers[$i] = 0;
                            $notFilling[$i] = true;
                        }
                    }
                } else {
                    $answers[$i] = 0;
                    $notFilling[$i] = true;
                }
            }

            $newAnswers[$i] = $answers[$i];
        }
        $answers = $newAnswers;

        if (isset($toolParams["checksNumber"]) && ($toolParams["checksNumber"] > 1)) {
            foreach ($answers as &$ans) {
                ksort($ans);
            }
        }

        return $notFilling;
	}

	function getFileNameByName($name)
	{
	    $this->rel();
	    $result = $this->field("jar_name", array("`{$this->name}`.`name`" => $name));
	    if (! is_string($result) || empty($result)) {
	        $result = null;
	    }

	    return $result;
	}

	function getAppletsMap()
	{
        $file_path = Application::docRootPath().Application::ds()."applets".Application::ds()."map.txt";
        $hash = array();
        if (file_exists($file_path)) {
            $lines = file($file_path);
            foreach ($lines as $line) {
                $line = trim($line);
                if (! strlen($line)) {
                    continue;
                }
                $tmp = split(" ", $line);
                if (isset($tmp[1])) {
                    $hash[$tmp[0]] = $tmp[1];
                }
            }
        }

        return $hash;
	}

	function setAppletsMap($map)
	{
        $file_path = Application::docRootPath().Application::ds()."applets".Application::ds()."map.txt";
        $lines = array();
        foreach ($map as $hash => $filename) {
            $lines[] = "{$hash} {$filename}";
        }
        file_put_contents($file_path, implode(PHP_EOL, $lines));
	}

	function getTypes()
	{
	    return $this->types;
	}

	function getTypesList()
	{
	    $result = array();
	    foreach ($this->types as $type) {
	        $result[$type] = __d('tests', 'type_'.$type, true);
	    }
	    return $result;
	}
}

class ToolParams
{
    private $name;
    private $mode;
    private $type;
    private $questionsNumber;
    private $answersNumber;
    private $maxAnswerNumber;
    private $inGroupNumber;
    private $checksNumber;
    private $binaryCombinationsNum;
    private $binaryCombinations;
    private $binaryCombinationsQuestions;

    function __construct($name)
    {
        $this->name = str($name)->toUpperCase()->value;

        $this->type = 'TEST';
        $this->checksNumber = 1;
        $this->binaryCombinationsNum = 0;
        $this->binaryCombinationsQuestions = array();
        $this->binaryCombinations = array();

        /**
         * @todo Реализовать проверку параметров инструментов для возможности
         * безопасного изменения/добавления инструментов.
         */

        switch ($this->name) {

        case 'NCTPM1':
            $this->questionsNumber = 32;
            $this->answersNumber = 5;
            $this->inGroupNumber = 8;
            break;

        case 'NATPM1':
            $this->questionsNumber = 35;
            $this->answersNumber = 5;
            $this->inGroupNumber = 6;
            $this->binaryCombinationsNum = 5;
            $this->binaryCombinationsQuestions = array(
                2, 4, 6, 7, 8, 9, 10, 12, 14, 15, 17, 18, 19, 20, 22, 25, 26, 27
            );
            break;

        case 'VCTPM1':
            $this->questionsNumber = 32;
            $this->answersNumber = 3;
            $this->inGroupNumber = 4;
            break;

        case 'VATPM1':
            $this->questionsNumber = 52;
            $this->answersNumber = 3;
            $this->inGroupNumber = 4;
            break;

        case 'PIU28':
            $this->type = 'QUEST';
            $this->questionsNumber = 77;
            $this->answersNumber = 4;
            $this->inGroupNumber = 10;
            $this->checksNumber = 2;
            break;

        case 'MU1':
            $this->type = 'QUEST';
            $this->questionsNumber = 144;
            $this->answersNumber = 5;
            $this->inGroupNumber = 18;
            break;

        default:
            throw new ArgException("Illegal tool name", 2050);
        }

        if ($this->binaryCombinationsNum > 0) {
            $this->binaryCombinations = $this->getAnswersBinaryCombinations($this->answersNumber, $this->binaryCombinationsNum);
        }

        $this->mode = "NORMAL";
        if ($this->checksNumber > 1) {
            $this->mode = "OPPOSITE";
        } elseif ($this->binaryCombinationsNum > 0) {
            $this->mode = "BIN_COMBS";
        }

        $this->maxAnswerNumber = $this->answersNumber + $this->binaryCombinationsNum;

        if ($this->maxAnswerNumber > 35) {
            throw new ArgException("Too many answers to a question", 2053);
        }
    }

    function getParams()
    {
        return array(
            'name' => $this->name,
            'mode' => $this->mode,
            'type' => $this->type,
            'questionsNumber' => $this->questionsNumber,
            'answersNumber' => $this->answersNumber,
            'maxAnswerNumber' => $this->maxAnswerNumber,
            'inGroupNumber' => $this->inGroupNumber,
            'checksNumber' => $this->checksNumber,
            'binaryCombinationsNum' => $this->binaryCombinationsNum,
            'binaryCombinationsQuestions' => $this->binaryCombinationsQuestions,
            'binaryCombinations' => $this->binaryCombinations,
        );
    }

	/**
	 * Возвращает массив с числовыми ключами,
	 * указывающими на подмассивы с номерами ответов.
	 * Для комбинированых ответов.
	 *
	 * @return array of array
	 */
	private function getAnswersBinaryCombinationsPrepare($addCombinations)
	{
	    $answersNum = $this->answersNumber;
        if (! is_int($addCombinations) || ($addCombinations < 0)) {
            throw new ArgException();
        }

	    if (($answersNum == 1) || ($addCombinations == 0)) {
	        return array();
	    }
	    $result = array();
	    $primaryAnswer = 1;
	    $i = 0;
	    do {
	        if ($primaryAnswer >= $answersNum) {
                throw new ArgException("Illegal binary combination for this question", 2051);
	        }
	        $maxSecondaryAnswers = $answersNum - $primaryAnswer;
	        $maxSecondaryAnswer = ($i + $maxSecondaryAnswers > $addCombinations)
	           ? $primaryAnswer + ($addCombinations - $i)
	           : $answersNum;
	        $secondaryAnswers = range($primaryAnswer + 1, $maxSecondaryAnswer);
            $result[$primaryAnswer] = $secondaryAnswers;
            $i += count($secondaryAnswers);
            $primaryAnswer++;
	    } while ($i < $addCombinations);

	    return $result;
	}

	/**
	 * Возвращает массив с числовыми ключами,
	 * указывающими на подмассивы с номерами ответов.
	 * Для комбинированых ответов.
	 *
	 * @return array of array
	 */
	private function getAnswersBinaryCombinations()
	{
	    $result = array();
	    foreach ($this->getAnswersBinaryCombinationsPrepare($this->binaryCombinationsNum)
	       as $primaryAnswer => $secondaryAnswers)
        {
            if (isset($result[$primaryAnswer])) {
                $result[$primaryAnswer] = ar::concat($result[$primaryAnswer], $secondaryAnswers);
            } else {
                $result[$primaryAnswer] = $secondaryAnswers;
            }
	        foreach ($secondaryAnswers as $secondaryAnswer) {
                $result[$secondaryAnswer][] = $primaryAnswer;
	        }
	    }

	    foreach ($result as &$array) {
	        $array = array_unique($array);
	    }

	    return $result;
	}

	/**
	 * Возвращает массив с числовыми значениями номеров вопросов в составе бинарной комбинации.
	 *
	 * @param array $number Значение, получившееся в результате комбинации.
	 * @return array
	 */
	function getAnswerBinaryCombination($number)
	{
        if (! is_int($number) || ($number < 1) || ($number > $this->maxAnswerNumber)) {
            throw new ArgException();
        }
	    if ($number <= $this->answersNumber) {
	        return array($number);
	    } else {
            $combinations = $this->getAnswersBinaryCombinationsPrepare($number - $this->answersNumber);
            $second = end(end($combinations));
            $first = key($combinations);
            return array($first, $second);
	    }
	}

	/**
	 * Возвращает номер ответа для комбинированных ответов.
	 *
	 * @param int $answer1 Номер первого ответа.
	 * @param int $answer2 Номер второго ответа.
	 * @return int
	 */
	function getAnswerNumByBinaryCombination($answer1, $answer2)
	{
	    $answersNum = $this->answersNumber;

	    if (($answer1 === $answer2)
            || ! is_int($answer1) || ($answer1 < 1) || ($answer1 > $answersNum)
            || ! is_int($answer2) || ($answer2 < 1) || ($answer2 > $answersNum))
        {
            throw new ArgException("Invalid arguments for ".__CLASS__."::".__FUNCTION__."()", 2052);
	    }

	    $minAnswer = min($answer1, $answer2);
	    $answer2 = max($answer1, $answer2);
	    $answer1 = $minAnswer;

	    $result = $answersNum;
	    for ($i = 1; $i < $answer1; $i++) {
	        $result += $answersNum - $i;
	    }
	    $result += $answer2 - $answer1 ;

	    return $result;
	}
}

?>